package org.unidata.mdm.rest.v1.meta.service.relations;

import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.meta.context.GetDataModelContext;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.dto.GetModelDTO;
import org.unidata.mdm.meta.dto.GetModelRelationDTO;
import org.unidata.mdm.rest.v1.meta.converter.RelationDefConverter;
import org.unidata.mdm.rest.v1.meta.converter.RelationDefinitionConverter;
import org.unidata.mdm.rest.v1.meta.ro.relations.DeleteRelationsResultRO;
import org.unidata.mdm.rest.v1.meta.ro.relations.GetRelationsResultRO;
import org.unidata.mdm.rest.v1.meta.ro.relations.RelationRO;
import org.unidata.mdm.rest.v1.meta.ro.relations.UpsertRelationsRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.relations.UpsertRelationsResultRO;
import org.unidata.mdm.rest.v1.meta.service.AbstractMetaModelRestService;
import org.unidata.mdm.system.service.TouchService;
import org.unidata.mdm.system.type.touch.Touch;
import org.unidata.mdm.system.type.touch.TouchParams;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;

/**
 * Relations rest controller
 *
 * @author Alexandr Serov
 * @since 24.11.2020
 **/
@Path(RelationRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class RelationRestService extends AbstractMetaModelRestService {

    /**
     * Service path.
     */
    public static final String SERVICE_PATH = "relations";

    /**
     * Service tag
     */
    public static final String SERVICE_TAG = "relations";

    /**
     * Has register data.
     */
    private static final Touch<Boolean> TOUCH_HAS_RELATION_DATA
        = Touch.builder(Boolean.class)
            .touchName("[touch-has-relation-data]")
            .paramType("entity-name", String.class)
            .paramType("relation-name", String.class)
            .build();
    /**
     * The TS.
     */
    @Autowired
    private TouchService touchService;

    @GET
    @Operation(
        description = "Gets relations list.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetRelationsResultRO find(
        @Parameter(description = "The draft id for drafts. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
        @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY) @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {

        GetModelDTO model = metaModelService.get(GetDataModelContext.builder()
                .allRelations(true)
                .draftId(resolveDraftId(draftId))
                .build());

        return new GetRelationsResultRO(model.getRelations().stream()
                .map(GetModelRelationDTO::getRelation)
                .map(RelationDefConverter::convert)
                .map(ro -> checkData.booleanValue() ? dataIndicator(ro) : ro)
                .collect(Collectors.toList()));
    }

    @GET
    @Path("outgoing/{name}")
    @Operation(
        description = "List of outgoing relations of an entity with the name 'name'. "
                    + "This entity is considered to be left side (or 'from') entity.",
        parameters = {
                @Parameter(in = ParameterIn.PATH, name = "name", description = "The name of the entity"),
                @Parameter(in = ParameterIn.QUERY, name = "draftId", description = "Draft id. Optional.")
        },
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetRelationsResultRO findEntitiesFrom(
            @PathParam("name") String entityName, @QueryParam("draftId") @DefaultValue("0") Long draftId,
            @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY) @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {
        return findEntities(entityName, draftId, true, checkData.booleanValue());

    }

    @GET
    @Path("incoming/{name}")
    @Operation(
        description = "List of incoming relations of an entity with the name 'name'. "
                    + "This entity is considered to be the right side (or 'to') entity.",
        parameters = {
                @Parameter(in = ParameterIn.PATH, name = "name", description = "The name of the entity"),
                @Parameter(in = ParameterIn.QUERY, name = "draftId", description = "Draft id. Optional.")
        },
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetRelationsResultRO findEntitiesTo(
            @PathParam("name") String entityName, @QueryParam("draftId") @DefaultValue("0") Long draftId,
            @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY) @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {
        return findEntities(entityName, draftId, false, checkData.booleanValue());
    }

    /**
     * Upsert.
     */
    @POST
    @Operation(
        description = "Add new/update existing relations.",
        method = HttpMethod.POST,
        tags = SERVICE_TAG)
    public UpsertRelationsResultRO upsert(UpsertRelationsRequestRO req) {
        Objects.requireNonNull(req, "Request can't be null");
        UpsertRelationsResultRO result = new UpsertRelationsResultRO();
        metaModelService.upsert(UpsertDataModelContext.builder()
            .name(req.getName())
            .relationsUpdate(RelationDefinitionConverter.convert(req.getRelations()))
            .draftId(resolveDraftId(req.getDraftId()))
            .build());
        result.setName(req.getName());
        return result;
    }

    /**
     * Delete.
     *
     * @param name the name
     * @return the response
     */
    @DELETE
    @Path("{name}")
    @Operation(
        description = "Remove relation.",
        method = HttpMethod.DELETE,
        tags = SERVICE_TAG)
    public DeleteRelationsResultRO delete(@Parameter(in = ParameterIn.PATH) @PathParam("name") String name,
                                          @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY)
                                          @QueryParam("draftId") @DefaultValue("0") Long draftId) {
        DeleteRelationsResultRO result = new DeleteRelationsResultRO();
        metaModelService.upsert(UpsertDataModelContext.builder()
            .relationsDelete(name)
            .draftId(resolveDraftId(draftId))
            .build());
        result.setId(name);
        return result;
    }

    private GetRelationsResultRO findEntities(String entityName, Long draftId, boolean fromDirection, boolean checkData) {

        GetModelDTO model = metaModelService.get(GetDataModelContext.builder()
            .entityIds(Collections.singletonList(entityName))
            .allFromRelations(fromDirection)
            .allToRelations(!fromDirection)
            .draftId(resolveDraftId(draftId))
            .build());

        return new GetRelationsResultRO(model.getRelations().stream()
                .map(GetModelRelationDTO::getRelation)
                .map(RelationDefConverter::convert)
                .map(ro -> checkData ? dataIndicator(ro) : ro)
                .collect(Collectors.toList()));
    }

    private RelationRO dataIndicator(RelationRO ro) {

        if (Objects.nonNull(ro)) {

            ro.setHasData(BooleanUtils.toBoolean(
                    touchService.singleTouch(new TouchParams<>(TOUCH_HAS_RELATION_DATA)
                            .with("relation-name", ro.getName())
                            .with("entity-name", ro.getFromEntity()))));
        }

        return ro;
    }
}
